Skip to content

fix(ownership): clear the enclosing killed set when a while condition contains break or continue - #13523

Open
asterite wants to merge 2 commits into
masterfrom
ab/while-condition-break-killed-tests
Open

fix(ownership): clear the enclosing killed set when a while condition contains break or continue#13523
asterite wants to merge 2 commits into
masterfrom
ab/while-condition-break-killed-tests

Conversation

@asterite

Copy link
Copy Markdown
Collaborator

Problem

The last-use analysis parks the enclosing loop's "guaranteed reassigned" facts (killed) on the side while it walks a nested while, so a break/continue written in that inner condition — which targets the enclosing loop — erased a scratch copy instead of the real facts. The compiler then kept a move decision the break had invalidated: the clone protecting

while i < n {
    let mut y = x;          // must clone: the break below can skip `x = [v, v, v]`
    y[0] = 9;
    ...
    while ({ if i == 1 { break; } j < 3 }) { j = j + 1; }   // targets the OUTER loop
    x = [v, v, v];          // skipped when the break fires
}

was never emitted, y[0] = 9 wrote through x's buffer in place, and a release nargo silently returned [9, 9] instead of [1, 9] (a debug build rejects the program via the rc_invariant validator instead).

This is the residual half of noir#13153: that fix propagated has_break across exactly this save/restore boundary but not killed, and the break_dependent_uses set it introduced is consulted only by the confirmed_moves partition, never by loop-exit truncation.

Fixes noir-lang/noir-claude#1702.

Fix

Mirror the existing has_break propagation for killed: after restoring the enclosing set, clear it when the condition contained a break/continue — the same effect the jump would have had if the enclosing facts had been in place when it was traversed.

Tests

The first commit is red (against the buggy compiler), the second is the fix:

  • execution_success/regression_while_condition_break_killed and regression_while_condition_continue_killed (the continue witness fires on the final iteration, so no later reassignment masks the corruption). Both sit alongside the three regressions noir#13153 shipped, which keep passing — the fix does not over-clone the shapes they pin.
  • Two ownership unit tests snapshotting the move/clone decision at the pass boundary; the fix flips their snapshots from let mut y$l5 = x$l2; to let mut y$l5 = x$l2.clone();.

Validation: full noirc_frontend (2228) and noirc_evaluator (1961) suites, all 128 loop/break-related execute tests, AST fuzzer smoke, clippy, fmt.

🤖 Generated with Claude Code

asterite and others added 2 commits August 13, 2026 15:02
…while-condition break/continue

A break or continue written in a nested while loop's condition targets
the ENCLOSING loop, so it can skip a reassignment below it. The
break/continue arm clears the killed set to record exactly that, but
find_last_uses_in_loop_body moves the enclosing killed set aside
(std::mem::take) before traversing the body and condition and restores
it unconditionally afterwards, discarding the clear. Loop-exit
truncation then wrongly exempts the variable, its use is classified as
a move, and the required clone is never emitted: a release nargo
silently returns [9, 9] instead of [1, 9], and a debug build rejects
the program via the rc_invariant validator.

Red tests only (residual of noir#13153, reported in noir-claude#1702):
two execution_success programs (break and continue spellings) and two
ownership unit tests whose snapshots record the buggy move decision.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…g to the enclosing loop's killed set

A variable is exempt from loop-exit clone-forcing only while its
in-loop reassignment is unconditional; a reachable break/continue
voids that, which the Break/Continue arm records with killed.clear().
But find_last_uses_in_loop_body saves the enclosing killed set away
(std::mem::take) while traversing the body and condition, so a
break/continue in a while condition — which targets the enclosing
loop — cleared the loop-local set, and the stale enclosing kills were
restored afterwards. The variable then stayed exempt, its use was
classified as a move, and the required clone was never emitted.

Mirror the existing has_break propagation: when the condition
contained a break/continue, clear the restored killed set too.

Fixes noir-lang/noir-claude#1702 (residual of noir#13153).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@asterite
asterite requested a review from TomAFrench August 14, 2026 16:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant